//
//  Laboratory.cpp
//  GPUProcessingTesting
//
//  Created by kadlubek47 on 12.07.2013.
//  Copyright (c) 2013 kadlubek47. All rights reserved.
//

#include <sstream>

#include "Laboratory.h"

#include "SpeedTestProc.h"
#include "MultiChannTestProc_many.h"
#include "MultiChannTestProc_one.h"

#include "Experiment.h"



#pragma mark Helper function

template <class PrintedClass>
void printArray(PrintedClass* arr, int size, std::ostream& stream = std::cout ) {
    
    for(int i=0; i<size; ++i) {
        stream << "[" << i << "] " << arr[i] << std::endl;
    }
    stream << std::endl;
    
}

void printTime(double time, std::ostream& stream = std::cout ) {
    stream << time << "s\t" << time * 1000 /*thousand*/ << "ms\t" << time*1000000 /*billion*/ << "micro_s";
}



#pragma mark Experiments

void Laboratory::multiChannelExperiment() {

    //Main description
    std::stringstream stringstream;
    stringstream << ("Multi channel experiment: if it is better to put\n"
                     "each channel to seperate opencl buffer or\n"
                     "all channels in one buffer.\n");
    const std::string mainDescription = stringstream.str();
    stringstream.clear();
    //.
    
    //Parameters
    const unsigned int blockSize = 512;
    const unsigned int repetition = 100;
    const std::string  fileName = std::string("MultiChannelExperiment.txt"); //<< result file name
    //.
    
    //----------------------------------------------------------------------------------------------------------------
    //EXPERIMENT
    Experiment multiChannelExperiment( mainDescription, fileName, resultsPath_ );
    
    multiChannelExperiment.add_trialsSet(      bufferForEachChannel_testFunction    (16, blockSize, repetition)       );
    multiChannelExperiment.add_trialsSet(      bufferForEachChannel_testFunction    (1, blockSize, repetition)        );
    //
    multiChannelExperiment.add_trialsSet(      oneBufferForAllChannels_testFunction (16, blockSize, repetition)       );
    multiChannelExperiment.add_trialsSet(      oneBufferForAllChannels_testFunction (1, blockSize, repetition)        );
    
    multiChannelExperiment.writeTo_stdcout  (false);
    multiChannelExperiment.writeTo_file     (false);
    //.
    //----------------------------------------------------------------------------------------------------------------
}

void Laboratory::minimumBufferSizeExperiment() {
    
    //Main description
    std::stringstream stringstream;
    stringstream << ("Minimum buffer size Experiment.\n"
                     "There must be trials set investigation.\n");
    const std::string mainDescription = stringstream.str();
    stringstream.clear();
    //.
    
    //Parameters
    const unsigned int minimumBlockSize = 512, maximumBlockSize = 512;
    const unsigned int repetition = 200;
    const std::string fileName = std::string("minimumBufferSizeExperiment.txt"); //<< result file name
    //.
    
    //----------------------------------------------------------------------------------------------------------------
    //EXPERIMENT
    Experiment multiChannelExperiment( mainDescription, fileName, resultsPath_ );
    
    for(unsigned int blockSize = minimumBlockSize; blockSize <= maximumBlockSize; blockSize*=2)
        multiChannelExperiment.add_trialsSet(      oneBufferForAllChannels_testFunction (2, blockSize, repetition)       );
    
    multiChannelExperiment.writeTo_stdcout  (true);
    multiChannelExperiment.writeTo_file     (true);
    //.
    //----------------------------------------------------------------------------------------------------------------
}

void Laboratory::latencyBandwidthExperiment( unsigned int sizeOfDataTransfered, unsigned int repetition) {
    using namespace std;
    const unsigned int MB_in_bytes = 1048576;
    SpeedTestProc openCLProc;
    SimplyTimer timer;
    
    
    
    //Latency
    openCLProc.setBuffers(1);  //< 1float transfering
    timer.start();
    for(int i=0; i < repetition; ++i)
        openCLProc.transferDataToGPU();
    timer.stop();
    //
    double averageLatency = timer.getTime() / repetition;
    //
    cout << "Latency: "; printTime(averageLatency); cout << endl;
    
    
    
    //Speed
    openCLProc.setBuffers( sizeOfDataTransfered );
    timer.start();
    for(int j=0; j < repetition; ++j)
        openCLProc.transferDataToGPU();
    timer.stop();
    //
    double bandwidth = sizeOfDataTransfered/(timer.getTime()/repetition-averageLatency)/MB_in_bytes;
    //
    cout << "Bandwidth (size of data transfered: "<< sizeOfDataTransfered << "): "<< bandwidth << " MB/s" << endl;
    cout << "Time per one transfer: "; printTime(timer.getTime()/repetition); cout << endl << endl;
    
}



#pragma mark Speed, latency tester

TrialsSet Laboratory::bufferForEachChannel_testFunction (const unsigned int numOfChannels, const unsigned int lengthOfBlock, const unsigned int numOfBlocks)
{
    using namespace std;
    
    MultiChannTestProc_many tester;
    Timer timer;
    double timeOfCallCommandsFun = 0;
    double timeOfFlushAndFinish = 0;
    
    
    //Allocation
    float*** blocksIn, ***blocksOut;
    blocksIn = new float**[numOfBlocks];
    blocksOut = new float**[numOfBlocks];
    
    for(int i = 0; i < numOfBlocks; ++i) {
        blocksIn[i] = new float*[numOfChannels];
        blocksOut[i] = new float*[numOfChannels];
    }
    
    for(int i = 0; i < numOfBlocks; ++i)
        for(int j = 0; j < numOfChannels; ++j) {
            blocksIn[i][j] = new float [lengthOfBlock];
            blocksOut[i][j] = new float [lengthOfBlock];
        }
    //.
    
    //Init source
    for(int i = 0; i < numOfBlocks; ++i)
        for(int j = 0; j < numOfChannels; ++j)
            for(int l = 0; l < lengthOfBlock; ++l)
                blocksIn[i][j][l] = l + i*lengthOfBlock;
    //.
    
    //Create buffers
    tester.process(blocksIn[0], numOfChannels, lengthOfBlock, blocksOut[0], &timeOfCallCommandsFun, &timeOfFlushAndFinish);
    timeOfCallCommandsFun = 0;
    timeOfFlushAndFinish = 0;
    //.
    
    //TEST
    timer.start(Timer::MAIN_TIMER);
    for(int i = 0; i < numOfBlocks; ++i) {
        timer.start(Timer::TRIAL_TIMER);
        tester.process(blocksIn[i], numOfChannels, lengthOfBlock, blocksOut[i], &timeOfCallCommandsFun, &timeOfFlushAndFinish);
        timer.stop (Timer::TRIAL_TIMER);
    }
    timer.stop(Timer::MAIN_TIMER);
    //.
    
    //Check
    bool successCopy = true;
    for(int i = 0; i < numOfBlocks; ++i)
        for(int j = 0; j < numOfChannels; ++j)
            for(int l = 0; l < lengthOfBlock; ++l)
                if( blocksIn[i][j][l] != blocksOut[i][j][l] ) {
                    successCopy = false;
                }
    
    
    if(!successCopy)
        throw NotExpectedProcessingResult_Exception();
    //.
    
    /*stringstream stringStream;
    stringStream << "***" << endl;
    stringStream << "Buffer for each channel." << endl;
    stringStream << "***" << endl;
    stringStream << "Time of calling commands functions (sum): " << timeOfCallCommandsFun*1000 << "ms" << endl;
    stringStream << "Time of flushing and finishing (sum): " << timeOfFlushAndFinish * 1000 << "ms" << endl;
    stringStream << "Proportion: " << timeOfCallCommandsFun/timeOfFlushAndFinish << " : 1" << endl;
    stringStream << "***" << endl;
    stringStream << "Number of channels: " << numOfChannels << endl;
    stringStream << "Number of blocks: " << numOfBlocks << endl;
    stringStream << "Block size: " << lengthOfBlock << endl;
    stringStream << "Average time: \t\t\t\t\t\t"; printTime(averageTime, stringStream); stringStream << endl;
    stringStream << "Maximum DSP (44100Hz): "; printTime(maximumDSP, stringStream); stringStream << endl;*/
    //Prepare result TrialsSet<double>
    TrialsSet trialsSet;
    trialsSet.add_trials    ( timer.get_trialsTimings() );
    trialsSet.add_mainTime  ( timer.get_mainTiming()    );
    
    using namespace std;
    trialsSet.add_descriptionElement( string(BLOCK_SIZE), lengthOfBlock );
    trialsSet.add_descriptionElement( string(SAMPLE_RATE),(unsigned int)STANDARD_SAMPLE_RATE );
    
    trialsSet.add_descriptionElement( string(HARDWARE), string("OPENCL_GPU") );
    trialsSet.add_descriptionElement( string(MANY_ONE_BUFFER), string("many"));
    trialsSet.add_descriptionElement( string(WORK_NAME), string("the simplest audio kernel"));
    
    trialsSet.add_descriptionElement( string(NUM_OF_CHANNELS), numOfChannels );
    //.
    
    return trialsSet;
}

TrialsSet Laboratory::oneBufferForAllChannels_testFunction (const unsigned int numOfChannels, const unsigned int lengthOfBlock, const unsigned int numOfBlocks)
{
    using namespace std;
    
    //Declarations
    MultiChannTestProc_one tester;
    Timer timer;
    
    double timeOfCallCommandsFun = 0;
    double timeOfFlushAndFinish = 0;
    
    float** blocksIn, **blocksOut;
    blocksIn = new float*[numOfBlocks];
    blocksOut = new float*[numOfBlocks];
    //.
    
    //Allocation for in, out
    for(int i = 0; i < numOfBlocks; ++i) {
        blocksIn[i]  = new float [lengthOfBlock * numOfChannels];
        blocksOut[i] = new float [lengthOfBlock * numOfChannels];
    }
    //.
    
    //Init in
    for(int i = 0; i < numOfBlocks; ++i)
        for(int l = 0; l < (lengthOfBlock * numOfChannels); ++l)
            blocksIn[i][l] = l;
    //.
    
    //Create gpu buffers
    tester.process(blocksIn[0], numOfChannels, lengthOfBlock, blocksOut[0], &timeOfCallCommandsFun, &timeOfFlushAndFinish);
    timeOfCallCommandsFun = 0;
    timeOfFlushAndFinish = 0;
    //.
    
    //MEASUREMENT
    timer.start(Timer::MAIN_TIMER);
    for(int i = 0; i < numOfBlocks; ++i) {
        timer.start(Timer::TRIAL_TIMER);
        tester.process(blocksIn[i], numOfChannels, lengthOfBlock, blocksOut[i], &timeOfCallCommandsFun, &timeOfFlushAndFinish);
        timer.stop (Timer::TRIAL_TIMER);
    }
    timer.stop(Timer::MAIN_TIMER);
    //.
    
    //Check
    bool successCopy = true;
    for(int i = 0; i < numOfBlocks; ++i)
        for(int l = 0; l < (lengthOfBlock * numOfChannels); ++l)
            if( blocksIn[i][l] != blocksOut[i][l] ) {
                successCopy = false;
            }
    if(!successCopy)
        throw NotExpectedProcessingResult_Exception();
    //.
    
    //Printing
    /*stringstream stringStream;
    stringStream << "***" << endl;
    stringStream << "One buffer for all channels." << endl;
    stringStream << "***" << endl;
    stringStream << "Time of calling commands functions (sum): " << timeOfCallCommandsFun*1000 << "ms" << endl;
    stringStream << "Time of flushing and finishing (sum): " << timeOfFlushAndFinish * 1000 << "ms" << endl;
    stringStream << "Proportion: " << timeOfCallCommandsFun/timeOfFlushAndFinish << " : 1" << endl;
    stringStream << "***" << endl;
    stringStream << "Number of channels: " << numOfChannels << endl;
    stringStream << "Number of blocks: " << numOfBlocks << endl;
    stringStream << "Block size: " << lengthOfBlock << endl;
    stringStream << "Average time: \t\t\t\t\t\t"; printTime(averageTime, stringStream); stringStream << endl;
    stringStream << "Maximum DSP (44100Hz): "; printTime(maximumDSP, stringStream); stringStream << endl;
    //.
    
    //Prepare result TrialsSet<double>
    double* trials;
    unsigned int trialsNumber;
    std::string description(stringStream.str());
    timer.getTrialsTimes ( (const double**)&trials, &trialsNumber );
    TrialsSet<double> trialsSet(trials, trialsNumber, description );*/
    
    return TrialsSet();
    //.
}
